// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.Extensions.DependencyInjection;

namespace SnapObjects.Expressions
{
    internal class CompilePolicyProvider : IPolicyProvider<Expression>
    {
        protected readonly CompileMethodMatcher _matcher;
        protected readonly CompileTypeSynchronizer _synchronizer;
        protected readonly IMetadataManager<Expression> _metadatas;
        protected readonly NestParseProvider<Expression> _nestParse;

        public CompilePolicyProvider(IServiceProvider provider)
        {
            _synchronizer = new CompileTypeSynchronizer();
            _matcher = provider.GetService<CompileMethodMatcher>();
            _metadatas = provider.GetService<IMetadataManager<Expression>>();
            _nestParse = provider.GetService<NestParseProvider<Expression>>();

            this.NestParser = new NestParseProvider<Expression>(provider);
        }

        public bool SupportShortCircuit => false;
        public INestParseProvider<Expression> NestParser { get; }

        public bool ShortCircuit(Expression test)
        {
            throw new InvalidOperationException();
        }

        public virtual Expression Addition(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.Add, this.GetArguments(left, right));
            }

            Expression stringFunc(Expression x, Expression y)
                => DynamicCall.GetExpression(nameof(String.Concat), null, x, y);

            return this.StringOperator(left, right, options, stringFunc, Expression.Add, true);
        }

        public virtual Expression Subtraction(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.Subtract, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right, options: options);

            return Expression.Subtract(left, right);
        }

        public virtual Expression Multiply(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.Multiply, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right, options: options);

            return Expression.Multiply(left, right);
        }

        public virtual Expression Division(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.Division, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right, options: options);

            return Expression.Divide(left, right);
        }

        public virtual Expression Modulus(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.Modulus, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right, options: options);

            return Expression.Modulo(left, right);
        }

        public virtual Expression Caret(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.Caret, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right, options: options);

            return Expression.Power(left, right);
        }

        public Expression Condition(
            Expression test, Expression ifTrue, Expression ifFalse)
        {
            test = Expression.Convert(test, typeof(bool));

            if (ifTrue.Type == typeof(object) || ifFalse.Type == typeof(object))
            {
                ifTrue = Expression.Convert(ifTrue, typeof(object));

                ifFalse = Expression.Convert(ifFalse, typeof(object));
            }
            else
            {
                _synchronizer.SynchronizeType(ref ifTrue, ref ifFalse);
            }

            return Expression.Condition(test, ifTrue, ifFalse);
        }

        public Expression List(IEnumerable<Expression> args)
        {
            // fix .netframework bug.
            if (!args.Any())
            {
                return Expression.Block(Expression.Empty());
            }

            return Expression.Block(args);
        }

        public Expression Constant(object value)
        {
            return Expression.Constant(value);
        }

        public Expression Not(Expression body)
        {
            return Expression.Not(body);
        }

        public Expression Negative(Expression body)
        {
            return Expression.Negate(body);
        }

        public Expression OrElse(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.OrElse, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right);

            return Expression.OrElse(left, right);
        }

        public Expression AndAlso(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.AndAlso, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right);

            return Expression.AndAlso(left, right);
        }

        public Expression And(Expression left, Expression right)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.And, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right);

            return Expression.And(left, right);
        }

        public Expression Or(Expression left, Expression right)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.Or, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right);

            return Expression.Or(left, right);
        }

        public Expression Constant(object value, Type type)
        {
            return Expression.Constant(value, type);
        }

        public Expression Equal(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.Equal, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right);

            return Expression.Equal(left, right);
        }

        public Expression NotEqual(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.NotEqual, this.GetArguments(left, right));
            }

            _synchronizer.SynchronizeType(ref left, ref right);

            return Expression.NotEqual(left, right);
        }

        public Expression LessThan(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.LessThan, this.GetArguments(left, right));
            }

            Expression stringFunc(Expression x, Expression y) =>
                Expression.LessThan(this.StringCompare(x, y), Expression.Constant(0));

            return this.StringOperator(left, right, options, stringFunc, Expression.LessThan);
        }

        public Expression GreaterThan(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.GreaterThan, this.GetArguments(left, right));
            }

            Expression stringFunc(Expression x, Expression y) =>
                Expression.GreaterThan(this.StringCompare(x, y), Expression.Constant(0));

            return this.StringOperator(left, right, options, stringFunc, Expression.GreaterThan);
        }

        public Expression LessThanOrEqual(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.LessThanOrEqual, this.GetArguments(left, right));
            }

            Expression stringFunc(Expression x, Expression y) =>
                Expression.LessThanOrEqual(this.StringCompare(x, y), Expression.Constant(0));

            return this.StringOperator(left, right, options, stringFunc, Expression.LessThanOrEqual);
        }

        public Expression GreaterThanOrEqual(
            Expression left, Expression right, ParserOptions<Expression> options)
        {
            if (WrapShellOperator.Check(left, right))
            {
                return Expression.Invoke(
                    WrapShellOperator.GreaterThanOrEqual, this.GetArguments(left, right));
            }

            Expression stringFunc(Expression x, Expression y) =>
                Expression.GreaterThanOrEqual(this.StringCompare(x, y), Expression.Constant(0));

            return this.StringOperator(left, right, options, stringFunc, Expression.GreaterThanOrEqual);
        }

        public Expression Identifier(string identifier, ParserOptions<Expression> options)
        {
            var isOk = _metadatas.TryGetMetadata(
                MetadataType.Property, identifier, options, out var metadata);

            if (isOk && metadata is PropertyMetadata<Expression> propery)
            {
                return propery.Value;
            }

            isOk = _metadatas.TryGetMetadata(
                    MetadataType.Parameter, identifier, options, out var meta);

            if (isOk && meta is ParameterMetadata<Expression> parameter)
            {
                return parameter.Value;
            }

            throw new IdentifierNotSupportedException(identifier);
        }

        public bool TryInvokeMethod(
            string name,
            Expression args,
            ParserOptions<Expression> options,
            out Expression result)
        {
            result = null;

            if (args is BlockExpression block &&
                options is CompileParserOptions context)
            {
                var isOk = _metadatas.TryGetMetadata(
                    MetadataType.Method, name, options, out var metadata);

                if (isOk)
                {
                    var paramList = new List<Expression>();

                    var methods = (CompileMethodMetadata)metadata;

                    var arguments = block.Expressions
                        .Where(x => !(x is DefaultExpression))
                        .ToArray();

                    var methodInfo = methods.OverLoads.FirstOrDefault(
                        m => _matcher.MatchMethod(context, m, arguments, out paramList));

                    if (methodInfo != null)
                    {
                        result = Expression.Invoke(methodInfo.Body, paramList);

                        return true;
                    }
                }
            }

            return false;
        }

        private Expression StringCompare(Expression left, Expression right)
        {
            var comparer = DynamicCall.GetExpression(nameof(String.Compare), null, left, right);

            return comparer ?? throw new StringInvalidOperationException(left.Type, right.Type);
        }

        private Expression StringOperator(
            Expression left,
            Expression right,
            ParserOptions<Expression> options,
            Func<Expression, Expression, Expression> stringFunc,
            Func<Expression, Expression, Expression> normalFunc,
            bool force = false)
        {
            if (left.Type == typeof(string) || right.Type == typeof(string))
            {
                if (force)
                {
                    if (left.Type != typeof(string))
                    {
                        left = DynamicCall.GetExpression(nameof(left.ToString), left);
                    }

                    if (right.Type != typeof(string))
                    {
                        right = DynamicCall.GetExpression(nameof(right.ToString), right);
                    }
                }

                return stringFunc(left, right);
            }
            else
            {
                _synchronizer.SynchronizeType(ref left, ref right, options: options);

                if (left.Type == typeof(string) && right.Type == typeof(string))
                {
                    return stringFunc(left, right);
                }

                if (left.Type != right.Type)
                {
                    right = Expression.Convert(right, left.Type);
                }

                return normalFunc(left, right);
            }
        }

        private Expression[] GetArguments(Expression left, Expression right)
        {
            return new Expression[]
            {
                Expression.Convert(left, typeof(object)),
                Expression.Constant(left.Type),
                Expression.Convert(right, typeof(object)),
                Expression.Constant(right.Type),
            };
        }
    }
}
